Android指令集以及smali文件
Dalvik是Google公司自己设计用于Android平台的虚拟机,和传统的JVM一样,都是用来解释执行编译后的java代码,只是Dalvik相较于传统的JVM,Dalvik基于寄存器设计,这样的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。另外Dalvik允许在有限的内存中同时运行多个虚拟机的实例。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。将dex中的字节码用可阅读的字符串形式表现出来的语言。我们称之为smali,可以理解为Android字节码的反汇编语言。smali指令和语法是android逆向的基础,必须熟练掌握。
1 Dalvik字节码
1.1 指令特点
Dalvik指定在调用格式上模仿了C语言的调用约定。Dalvik指令的语法与助词符有如下特点:
- 参数采用从目标(destination)到源(source)的方式
- 根据字节码的大小与类型不同,一些字节码添加了名称后缀以消除岐义
- 32位常规类型的字节码末添加任何后缀, 64位常规类型的字节码添加 -wide后缀
- 特殊类型的字节码根据具体类型添加后缀,它们可以是 -boolean,-byte,-char,-short,-int,-long,-float,-double,-object,-string,-class,-void之一
- 根据字节码的布局与选项不同,一些字节码添加了字节码后缀以消除岐义。这些后缀通过在字节码主名称后添加斜杠“/”来分隔开
- 在指令集的描述中,宽度值中每个字母表示宽度为4位
例如这条指令:move-wide/from16 vAA, vBBBB
move为基础字节码(base opcode),标识这是基本操作。wide为名称后缀(name suffix),标识指令操作的数据宽度(64位)。from16为字节码后缀(opcode suffix),标识源为一个16位的寄存器引用变量。vAA为目的寄存器,它始终在源的前面,取值范围为v0~v255。vBBBB为源寄存器,取值范围为v0~v65535。指令集中大多数指令用到了寄存器作为目的操作数或源操作数,其中 A/B/C/D/E/F/G/H 代表一个4位的数值, 可用来表示v0~v15的寄存器。 AA/BB/…/HH代表一个8位的数值。 AAAA/BBBB/…/HHHH 代表一个16位的数值
1.2 寄存器的使用规则
在dalvik字节码中,寄存器都是32位的,能够支持任何类型。64位类型(Long和Double型)用2个寄存器表示。有两种方式指定一个方法中有多少寄存器是可用的。.registers指令指定了方法中寄存器的总数。.locals指令表明了方法中非参寄存器的数量。假设有一个实例方法compare(String x, String y),compare内部又使用了两个局部变量。在该方法中会使用了5个寄存器(v0-v4),参数使用最后的几个寄存器。另外关于反编译代码中寄存器的命名,有两种方式 V命名和P命名 。
寄存器 | V命名 | P命名 | 寄存器含义 |
---|---|---|---|
1 | v0 | v0 | 第一个局部寄存器 |
2 | v1 | v1 | 第二个局部寄存器 |
3 | v2 | p0 | 第一个参数寄存器 |
4 | v3 | p1 | 第二个参数寄存器 |
5 | v4 | p2 | 第三个参数寄存器 |
baksmali默认对参数寄存器使用P命名方式。如果想使用V命名方式,可以使用-pl—no-parameter-registers选项。使用P命名方式是为了防止以后如果要在方法中增加寄存器,需要对参数寄存器重新进行编号的缺点。
1.3 Dalvik描述符
与JVM相类似,Davilk字节码中同样有一套用于描述类型、方法、字段的方法,这些方法结合Davilk的指令便形成了完整的汇编代码。
1.3.1 字节码和数据类型
Davilk字节码只有两种类型 基本类型和引用类型 。对象和数组都是引用类型,Davilk中对字节码类型的描述和JVM中的描述符规则一致:对于基本类型和无返回值的void类型都是用一个大写字母表示,对象类型则用字母L加对象的全限定名来表示。以String为例,其完整名称是java.lang.String,那么其全限定名就是 java/lang/String; ,即java.lang.String的 . 用 / 代替,并在末尾添加分号 ; 做结束符。数组则用 [ 来表示,具体规则如下所示:
java类型 | 类型描述符 |
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | J |
float | F |
double | D |
void | V |
对象类型 | L |
数组类型 | [ |
1.3.2 数组类型
[ 用来表示所有基本类型的数组, [ 后跟着是基本类型的描述符。每一维度使用一个前置的 [ 。比如java中的int[]用汇编码表示便是 [I; .二维数组int[][]为 [[I; ,三维数组则用 [[[I; 表示。
1.3.3 对象类型
L可以表示java类型中的任何类。在java代码中以package.name.ObjectName的方式引用,而在Davilk中其描述则是以Lpackage/name/ObjectName;的形式表示。L即上面定义的java对象类型,表示后面跟着的是类的全限定名。比如java中的java.lang.String对应的描述是Ljava/lang/String;。
1.3.4 字段的描述
Davilk中对字段的描述分为两种,对基本类型字段的描述和对引用类型的描述,但两者的描述格式一样: 对象类型描述符->字段名:类型描述符; 。比如com.sbbic.Test类中存在String类型的name字段的描述为 Lcom/sbbic/Test;->name:Ljava/lang/String; 。
1.3.5 方法的描述
java中方法的签名包括方法名,参数及返回值,在Davilk相应的描述规则为: 对象类型描述符->方法名(参数类型描述符)返回值类型描述符 。比如java方法public char charAt(int index)的描述为 Ljava/lang/String;->charAt(I)C 。
1.4 Dalvik指令集
掌握以上的字段和方法的描述,只能说我们懂了如何描述一个字段和方法,而关于方法中具体的逻辑则需要了解Dalvik中的指令集,因为Dalvik是基于寄存器的架构的,因此指令集和JVM中的指令集区别较大,反而更类似x86的中的汇编指令。
1.4.1 数据定义指令
数据定义指令用于定义代码中使用的常量,类等数据,基础指令是const
指令 | 描述 |
---|---|
const/4 vA,0x01 | 将数值符号扩展为32后赋值给寄存器vA |
const-wide/16 vAA,0x01 | 将数值符号扩展为64位后赋值个寄存器对vAA |
const-string vAA,"test" | 字符串赋值给寄存器vAA |
const-class vAA,Lo/ag; | 将一个类的引用赋值给寄存器vAA |
1.4.2 数据操作指令
move指令用于数据操作,其表示move destination,source,即数据数据从source寄存器移动到destionation寄存器,可以理解java中变量间的赋值操作。根据字节码和类型的不同,move指令后会跟上不同的后缀。
指令 | 描述 |
---|---|
move vA,vB | 将vB寄存器的值赋值给vA寄存器,vA和vB寄存器都是4位 |
move/from16 vAA,VBBBB | 将vBBBB寄存器(16位)的值赋值给vAA寄存器(7位),from16表示源寄存器vBBBB是16位的 |
move/16 vAAAA,vBBBB | 将寄存器vBBBB的值赋值给vAAAA寄存器,16表示源寄存器vBBBB和目标寄存器vAAAA都是16位 |
move-object vA,vB | 将vB寄存器中的对象引用赋值给vA寄存器,vA寄存器和vB寄存器都是4位 |
move-result vAA | 将上一个invoke指令(方法调用)操作的单字(32位)非对象结果赋值给vAA寄存器 |
move-result-wide vAA | 将上一个invoke指令操作的双字(64位)非对象结果赋值给vAA寄存器 |
mvoe-result-object vAA | 将上一个invoke指令操作的对象结果赋值给vAA寄存器 |
move-exception vAA | 保存上一个运行时发生的异常到vAA寄存器 |
1.4.3 对象操作指令
与对象实例相关的操作,比如对象创建,对象检查等。
指令 | 描述 |
---|---|
new-instance vAA,type@BBBB | 构造一个指定类型的对象将器引用赋值给vAA寄存器.此处不包含数组对象 |
instance-of vA,vB,type@CCCC | 判断vB寄存器中对象的引用是否是指定类型,如果是,将v1赋值为1,否则赋值为0 |
check-cast vAA,type@BBBB | 将vAA寄存器中对象的引用转成指定类型,成功则将结果赋值给vAA,否则抛出ClassCastException异常. |
1.4.4 数组操作指令
在实例操作指令中我们并没有发现创建对象数组的指令。Davilk中设置专门的指令用于数组操作。
指令 | 描述 |
---|---|
new-array vA,vB,type@CCCC | 创建指定类型与指定大小(vB寄存器指定)的数组,并将其赋值给vA寄存器 |
fill-array-data vAA,+BBBBBBBB | 用指定的数据填充数组,vAA代表数组的引用(数组的第一个元素的地址) |
1.4.5 运算指令
数据运算指令包含算术运算指令与逻辑运算指令以及位移指令。下面的-type表示操作的寄存器中数据的类型,可以是-int,-float,-long,-double等。
指令 | 描述 |
---|---|
add-type | 加法指令 |
sub-type | 减法指令 |
mul-type | 乘法指令 |
div-type | 除法指令 |
rem-type | 求模 |
and-type | 与运算指令 |
or-type | 或运算指令 |
xor-type | 异或元算指令 |
shl-type | 有符号左移指令 |
shr-type | 有符号右移指令 |
ushr-type | 无符号右移指令 |
1.4.6 字段读写
字段读写之类分为对象字段和静态字段,对象字段以i开头,静态字段以s开头。读取类型分为普通类型和对象类型object。下标中-type可以是-int,-float,-long,-double,-object等。
指令 | 描述 |
---|---|
iget-type | iget-byte vX,vY,filed_id表示读取vY寄存器中的对象中的filed_id字段值赋值给vX寄存器 |
iput-type | iput-byte vX,vY,filed_id表示设置vY寄存器中的对象中filed_id字段的值为vX寄存器的值 |
sget-type | sget-byte vX,vY,filed_id表示读取vY寄存器中的类中的filed_id字段值赋值给vX寄存器 |
sput-type | sget-byte vX,vY,filed_id表示设置vY寄存器中的类中filed_id字段的值为vX寄存器的值 |
1.4.7 方法调用指令
Davilk中的方法指令和JVM的中指令大部分非常类似。目前共有五条指令集:
指令 | 描述 |
---|---|
invoke-direct{parameters},methodtocall | 调用实例的直接方法,即private修饰的方法.此时需要注意{}中的第一个元素代表的是当前实例对象,即this,后面接下来的才是真正的参数 |
invoke-static{parameters},methodtocall | 调用实例的静态方法,此时{}中的都是方法参数 |
invoke-super{parameters},methodtocall | 调用父类方法 |
invoke-virtual{parameters},methodtocall | 调用实例的虚方法,即public和protected修饰修饰的方法 |
invoke-interface{parameters},methodtocall | 调用接口方法 |
这五种指令是基本指令,除此之外还有invoke-direct/range、invoke-static/range、invoke-super/range、invoke-virtual/range、invoke-interface/range指令,该类型指令和以上指令唯一的区别就是后者可以设置方法参数可以使用的寄存器的范围,在参数多于四个时候使用。
1.4.8 方法返回指令
在java中,很多情况下我们需要通过Return返回方法的执行结果,在Davilk中同样提供的return指令来返回运行结果
指令 | 描述 |
---|---|
return-void | 什么也不返回 |
return vAA | 返回一个32位非对象类型的值 |
return-wide vAA | 返回一个64位非对象类型的值 |
return-object vAA | 反会一个对象类型的引用 |
1.4.9 跳转指令
跳转指令用于从当前地址条状到指定的偏移处,在if、switch分支中使用的居多。Davilk中提供了goto、packed-switch、if-test指令用于实现跳转操作。
指令 | 描述 |
---|---|
goto +AA | 无条件跳转到指定偏移处(AA即偏移量) |
packed-switch vAA,+BBBBBBBB | 分支跳转指令,vAA寄存器中的值是switch分支中需要判断的,BBBBBBBB则是偏移表(packed-switch-payload)中的索引值, |
spare-switch vAA,+BBBBBBBB | 分支跳转指令,和packed-switch类似,只不过BBBBBBBB偏移表(spare-switch-payload)中包含索引值和偏移量 |
if-test vA,vB,+CCCC | 条件跳转指令,用于比较vA和vB寄存器中的值,如果满足则跳转,跳转可以是eq、lt等 |
2 Smali文件
通过baksmali.jar反编译出来每个.smali,都对应与java中的一个类,每个smali文件都是Davilk指令组成的,并遵循一定的结构.smali存在很多的指令用于描述对应的java文件,所有的指令都以 . 开头,常用的指令如下:
关键词 | 说明 |
---|---|
.filed | 定义字段 |
.method…end | method定义方法 |
.annotation…end | annotation定义注解 |
.implements | 定义接口指令 |
.local | 指定了方法内局部变量的个数 |
.registers | 指定方法内使用寄存器的总数 |
.prologue | 表示方法中代码的开始处 |
.line | 表示java源文件中指定行 |
.paramter | 指定了方法的参数 |
.param | 和.paramter含义一致,但是表达格式不同 |
2.0.1 文件头描述
smali文件的前三行描述了当前类的信息:
.class <访问权限修饰符> [非权限修饰符] <类名> .super <父类名> .source <源文件名称>
<>中的内容表示必不可缺的,[]表示的是可选择的。访问权限修饰符即所谓的public、rotected、private即default。而非权限修饰符则指的是final、abstract。举例说明:
.class public final Lcom/sbbic/demo/Device; .super Ljava/lang/Object; .source "Device.java"
2.0.2 文件正文
- 接口
如果该类实现了某个接口,则会通过.implements定义,其格式如下:
#interfaces .implements <接口名称>
- 注解
如果一个类中使用注解,会用.annotation定义,其格式如下:
#annotations .annotation [注解的属性] <注解类名> [注解字段=值] ... .end
- 字段
smali中使用.field描述字段,我们知道java中分为静态字段和普通字段,它们在smali中的表示如下:
# 普通字段 #instance fields .field <访问权限修饰符> [非权限修饰符] <字段名>:<字段类型> # 静态字段 #static fields .field <访问权限> static [修饰词] <字段名>:<字段类型>
- 方法
smali中使用.method描述方法,具体定义格式如下:
#direct methods 表示直接方法 #virtual methods 表示直接方法 .method <访问权限修饰符> [非访问权限修饰符] <方法原型> <.locals> [.parameter] [.prologue] [.line] <代码逻辑> .end